-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Store PasswordExpiry and OAuthRefreshToken #1464
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Store PasswordExpiry and OAuthRefreshToken #1464
Conversation
Add properties ICredential.PasswordExpiryUTC and ICredential.OAuthRefreshToken. These correspond to Git credential attributes password_expiry_utc and oauth_refresh_token, see https://git-scm.com/docs/git-credential#IOFMT. Previously these attributes were silently disarded. Plumb these properties from input to host provider to credential store to output. Credential store support for these attributes is optional, marked by new properties ICredentialStore.CanStorePasswordExpiryUTC and ICredentialStore.CanStoreOAuthRefreshToken. Implement support in CredentialCacheStore, SecretServiceCollection and WindowsCredentialManager. Add method IHostProvider.ValidateCredentialAsync. The default implementation simply checks expiry. Improve implementations of GenericHostProvider and GitLabHostProvider. Previously, GetCredentialAsync saved credentials as a side effect. This is no longer necessary. The workaround to store OAuth refresh tokens under a separate service is no longer necessary assuming CredentialStore.CanStoreOAuthRefreshToken. Querying GitLab to check token expiration is no longer necessary assuming CredentialStore.CanStorePasswordExpiryUTC.
a2425b9 to
a21c066
Compare
a21c066 to
2a14b66
Compare
|
Windows build ought to be fixed by #1418 |
83c3f3a to
b59547b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks good! Most of my comments are small things - the only major issue that I'd need to see updated before approving is not having expiry implemented for Windows Credential Manager.
01fcd91 to
fdd7759
Compare
fdd7759 to
60224e0
Compare
ldennington
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the updates!
Marshal.FreeHGlobal and Marshal.DestroyStructure
This comment has been minimized.
This comment has been minimized.
Limit iterations in GetCredentialAsync
PetSerAl
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My opinion (feal free to disregard it): it is better not to remove refresh token unnecessary. I understand, that GCM can just reauthenticate, but that require extra, otherwise unnecessary, browser interaction from user, thus providing slightly worse user experience.
Anyways, I have no intentions to spend my time to pursue this. Error rate unlikely would be that hi to bother me that much.
|
@mjcheetham any time to look at this? |
This comment has been minimized.
This comment has been minimized.
@hickford: @mjcheetham is no longer working at GitHub (but is once again my teammate, this time at Microsoft). Git Credential Manager, however, is still owned by GitHub and it is currently not clear to me who at GitHub takes care of this here repository. |
This comment has been minimized.
This comment has been minimized.
|
|
||
| // Return the new access token | ||
| return new GitCredential(oauthUser,refreshResult.AccessToken); | ||
| return new GitCredential(refreshResult, oauthUser); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Delaying the save of a refresh_token will likely result in issues.
New value must be saved, even if access_token is invalid for the requested operation.
This means we'd have to check (and rely on git) to also provide the new refresh_token and store its value during an erase callback (needs ugly hack in EraseCredentialAsync).
Granted the current state (without #1838) is also broken. (fix has been merged)
I think this is still the correct location to directly issue a store.
Otherwise there is a risk of client desync due to missed refresh_token value updates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are advantages and disadvantages to pre-emptively storing the refresh token. In particular, storing the wrong username if the user has multiple accounts on the host and selects the wrong account. In general, I'd prefer to avoid the complexity of side effects. The Git credential API has deliberate independent verbs "get/store/erase".
Which host do you test with?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Chances are high the 1st action is a pull from a repository readable by the 2nd account.
So any existing entry is likely to be replaced anyway (no failure as account guard). ☺
Git can only give feedback (store vs. erase) on the access_token.
Any provided refresh_token is (by convention) valid and MUST be used in further interactions.
Saving that during the get phase is not a side effect but required as part of a completed transaction.
Delaying the save can break things on Ctrl+C, parallel pulls, or stupid proxies
(see comments regarding missing token updates).
The primitive get with detached store/erase is broken in the context of OAuth.
A 2nd parallel action from the same host will trigger another token refresh and a conflicting store
(not an issue to have multiple access_token, but potentially fatal with an outdated refresh_token) .
My primary endpoint is Forgejo with recommended single-use tokens (→ #1838 required).
Primary client is Win11 (where leaking refresh_token in the output would be only a minor violation).
(→ getting to the next potential problem with the get/store approach on a proper secret store…)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay -- I've restored that logic. Would you be able to test this branch with Forgejo?
| ["password"] = testPassword | ||
| ["password"] = testPassword, | ||
| ["password_expiry_utc"] = testExpiry.ToString(), | ||
| ["oauth_refresh_token"] = testRefreshToken, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The refresh_token will likely also have an expiry date.
At least one observed implementation however does not report the constraint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any suggestion to improve the test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, wrong/stupid location for my comment (test is not the issue). ☹
(Maybe you can mark this part of my comments as resolved to avoid further confusion?)
General problem with the auth flow when not dealing with refresh in the get phase:
- presenting
gitwith an expired credential will just lead to ignoring it - the
refresh_tokenmight also be expired (butgitwould have no way of recovering anyway…)
Telling git should only be useful if there was a further provider with valid credentials as a fallback…
But since GCM has no way of knowing this, it's likely best to keep handling refresh proactively in get.
Which would of course render the whole concept of supplying any token metadata to git redundant.
Suggested-by: Marc Becker
|
Hm, lumping together these 2 distinct entities opens a can of worms when trying to do updates consistently.
So this would mean (to be consistent with current/fallback behavior):
When updating the The whole approach of partial updates also massively increases potential for conflicting parallel operations. |
|
To be honest, in the OAuth case I would opt to
This is likely to reduce potential conflicts even in the current approach. |
Add properties ICredential.PasswordExpiry and ICredential.OAuthRefreshToken. These correspond to Git credential attributes password_expiry_utc and oauth_refresh_token, see https://git-scm.com/docs/git-credential#IOFMT. Previously these attributes were silently disarded.
Plumb these properties from input to host provider to credential store to output.
Credential store support for these attributes is optional, marked by new properties CredentialStore.CanStorePasswordExpiry and ICredentialStore.CanStoreOAuthRefreshToken. Implement support in CredentialCacheStore, SecretServiceCollection and WindowsCredentialManager.
Add method IHostProvider.ValidateCredentialAsync. The default implementation simply checks expiry. Other implementations might query a server.
Improve implementations of GenericHostProvider and GitLabHostProvider. Previously, GetCredentialAsync saved credentials as a side effect. This is no longer necessary. The workaround to store OAuth refresh tokens under a separate service is no longer necessary assuming CredentialStore.CanStoreOAuthRefreshToken. Querying GitLab to check token expiration is no longer necessary assuming CredentialStore.CanStorePasswordExpiry.
Fixes #1463
Fixes #268
Fixes #2059